﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using Util.Forms;

namespace iPhoneBackupViewer
{
    public partial class BrowseBackupForm : Form
    {
        public BrowseBackupForm()
        {
            InitializeComponent();
        }

        private DirectoryInfo backupDir;

        // TODO: Add intelligent file format learning:
        // In the currently displayed folder, for each unknown extension,
        // we pick the first three files with that extension and make an
        // attempt to open them (e.g. by examining the first few bytes
        // and use the corresponding reader to open the file). If two 
        // or more of these three attempts succeeds with the same type,
        // we assume all files in this folder with this extension are of 
        // this type. If this type supports preview (such as JPG), we
        // will render the other files with this extension. If this type
        // doesn't support preview (such as .sqlitedb or .plist), we just
        // change the icon to indicate that we know its format.
        // 
        // Two optimizations are further employed. First, if the file is
        // of a known extension (e.g. JPG), it is assumed to be of that
        // type. Second, if actually opening the file is too costly (e.g.
        // if the file is .sqlitedb), we only check the header bytes to
        // determine its type.
        //
        // Likewise, if three files of a known extension cannot be opened,
        // we assume that this extension is no good for this folder, and
        // treat the extension as not supported.

        private void BrowseBackupForm_Load(object sender, EventArgs e)
        {
            tvFolders.SetWindowTheme("explorer");
            lvFiles.SetWindowTheme("explorer");

            ChooseBackupForm f = new ChooseBackupForm();
            if (f.ShowDialog(this) != System.Windows.Forms.DialogResult.OK)
                return;

            this.backupDir = f.SelectedDirectory;
            string filename = Path.Combine(backupDir.FullName, "Info.plist");
            //propertyGrid1.SelectedObject = PropertyList.Load(filename);

            MobileDatabase mbdb = new MobileDatabase(
                Path.Combine(backupDir.FullName, "Manifest.mbdb"));
            //propertyGrid1.SelectedObject = mbdb;

            HierarchyItem root = BuildHierarchy(mbdb.Records);
            propertyGrid1.SelectedObject = root;

            // Fill the treeview with the hierarchy.
            FillTreeView(root);
        }

        /// <summary>
        /// Here we want to precisely define how a path is constructed.
        /// A path has separator '/'. 
        /// 
        /// Each component in a path can be the empty string; therefore,
        /// two or more consecutive '/' s actually are significant.
        /// 
        /// If a path ends with a '/', it is assumed to be a directory,
        /// and the trailing '/' is trimmed from the directory name.
        /// Otherwise, it's assumed to be a file.
        /// 
        /// The component before the first '/' is the root folder. If a
        /// path starts with '/', then its root folder is assumed to be "",
        /// which is the default root folder.
        /// 
        /// If a path doesn't contain a '/', it is assumed to be a root
        /// item itself.
        /// </summary>
        /// <param name="items"></param>
        private HierarchyItem BuildHierarchy(IEnumerable<object> items)
        {
            HierarchyBuilder h = new HierarchyBuilder();
            foreach (object item in items)
            {
                h.AddItem(item.ToString(), item);
            }
            return h.Root;
        }

        private void FillTreeView(HierarchyItem root)
        {
            tvFolders.Nodes.Clear();

            var children = from x in root.Children
                           orderby x.Name
                           select x;
            foreach (HierarchyItem item in children)
            {
                if (item.IsDirectory)
                {
                    TreeNode node = tvFolders.Nodes.Add(item.Name);
                    node.Tag = item;
                    FillTreeView(item, node);
                }
            }
        }

        private void FillTreeView(HierarchyItem item, TreeNode node)
        {
            var children = from x in item.Children
                           orderby x.Name
                           select x;
            foreach (HierarchyItem child in children)
            {
                if (child.IsDirectory)
                {
                    TreeNode childNode = node.Nodes.Add(child.Name);
                    childNode.Tag = child;
                    FillTreeView(child, childNode);
                }
            }
        }

        private void tvFolders_AfterSelect(object sender, TreeViewEventArgs e)
        {
            HierarchyItem folder = e.Node.Tag as HierarchyItem;
            if (folder == null)
                return;

            // Fill the list view.
            lvFiles.Items.Clear();
            lvFiles.Visible = false;

            // We convert it to an array to avoid the overhead in
            // IEnumerable, though this in theory should not exist
            // as we sorted the collection.
            var children = (from file in folder.Children
                            orderby file.IsDirectory descending, file.Name
                            select file).ToArray();
            foreach (HierarchyItem file in children)
            {
                ListViewItem item = new ListViewItem();
                MobileFileInfo mfi = file.Tag as MobileFileInfo;
                item.Text = file.Name;
                if (mfi != null)
                {
                    if (!mfi.IsDirectory)
                        item.SubItems.Add(mfi.Length.ToString("0,0"));
                }
                item.Tag = mfi;
                lvFiles.Items.Add(item);
            }
            lvFiles.Visible = true;
            txtStatus.Text = lvFiles.Items.Count + " items";
        }

        private void btnExport_Click(object sender, EventArgs e)
        {
            if (lvFiles.SelectedIndices.Count == 0)
                return;

            MobileFileInfo fi = lvFiles.SelectedItems[0].Tag as MobileFileInfo;
            if (fi == null)
                return;
            if (fi.Mode.Type != MobileFileType.Regular)
                return;

            saveFileDialog1.FileName = Path.GetFileName(fi.Path);
            if (saveFileDialog1.ShowDialog(this) != DialogResult.OK)
                return;

            string oldFile = Path.Combine(this.backupDir.FullName, fi.Key);
            string newFile = saveFileDialog1.FileName;
            try
            {
                File.Copy(oldFile, newFile, true);
            }
            catch (Exception ex)
            {
                MessageBox.Show(this, "Cannot save to " + newFile + ": " +
                    ex.Message, "Export", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            MessageBox.Show("Saved " + newFile + ".", "Export",
                MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void lvFiles_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            if (lvFiles.SelectedIndices.Count == 0)
                return;

            MobileFileInfo fi = lvFiles.SelectedItems[0].Tag as MobileFileInfo;
            if (fi == null)
                return;
            if (fi.Mode.Type != MobileFileType.Regular)
                return;

            string ext = Path.GetExtension(fi.Path).ToLowerInvariant();
            if (ext.StartsWith(".sqlite") || ext == ".db")
            {
                SQLiteViewerForm f = new SQLiteViewerForm();
                f.FileName = Path.Combine(this.backupDir.FullName, fi.Key);
                f.ShowDialog(this);
            }
        }

        private void lvFiles_SelectedIndexChanged(object sender, EventArgs e)
        {

        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    class HierarchyItem
    {
        public string Name;
        public object Tag;
        public bool IsDirectory { get; private set; }
        public List<HierarchyItem> Children { get; private set; }

        public HierarchyItem(bool isDirectory)
        {
            if (isDirectory)
                this.Children = new List<HierarchyItem>();
            this.IsDirectory = isDirectory;
        }

        public override string ToString()
        {
            return Name;
        }
    }

    class HierarchyBuilder
    {
        // Path => tag
        Dictionary<string, HierarchyItem> dict = new Dictionary<string, HierarchyItem>();
        HierarchyItem root = new HierarchyItem(true);

        public HierarchyItem Root { get { return root; } }

        // Note: this has the special effect that a file and a directory
        // of the same name can co-exist. For example,
        //   parent/child
        //   parent/child/
        // can co-exist.
        public HierarchyItem AddItem(string path, object tag)
        {
            // If the path ends with a '/', it's assumed to be a directory;
            // otherwise, it's assumed to be a file.
            bool isDirectory = path.EndsWith("/");

            // Find the parent folder of this item.
            int k = isDirectory ?
                path.Substring(0, path.Length - 1).LastIndexOf('/') :
                path.LastIndexOf('/');
            string parent = (k >= 0) ? path.Substring(0, k + 1) : null;

            // Find the stripped name of this item relative to its parent.
            string name = path.Substring(k + 1);

            // Get a handle to this item's container. If this item
            // has a parent, the handle is its parent. If this item
            // doesn't have a parent (i.e. it is a root-level item),
            // then the handle is the containing namespace.
            HierarchyItem container;
            if (parent != null)
            {
                // If we haven't created the parent folder, create it first.
                if (!dict.TryGetValue(parent, out container))
                    container = AddItem(parent, null);
            }
            else
            {
                container = root;
            }

            // Add this item to the hierarchy. If an item with the same name
            // already exists, replace its tag (but not its contents, if the
            // existing item is a directory).
            HierarchyItem item;
            if (dict.TryGetValue(path, out item))
            {
                item.Tag = tag;
            }
            else
            {
                item = new HierarchyItem(isDirectory);
                item.Tag = tag;
                item.Name = name.TrimEnd('/');
                dict.Add(path, item);
                
                // Add this item to its parent.
                container.Children.Add(item);
            }
            return item;
        }
    }
}
